package com.environment;

import com.knowledge.facts.FriendlyHillFact;
import com.knowledge.facts.EnemyHillFact;
import com.knowledge.facts.UnexploredFact;
import com.knowledge.facts.FriendlyAntFact;
import com.knowledge.facts.FoodFact;
import com.knowledge.facts.FactRepository;
import com.knowledge.facts.EnemyAntFact;
import com.knowledge.facts.Fact;
import com.knowledge.facts.IntendedLocationFact;
import com.utils.AStarSearch;
import com.utils.InvalidException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Holds all game data and current game state.
 */
public class Percept {
    /** Maximum map size. */
    public static final int MAX_MAP_SIZE = 256;
    
    private int loadTime;
    
    private int turnTime;
    
    private int rows;
    
    private int cols;
    
    private int turns;
    
    private int viewRadius2;
    
    private int attackRadius2;
    
    private int spawnRadius2;
    
    private long turnStartTime;
    
    private Ilk map[][];
        
    private static Percept instance = null;
    
    private int killCount[];
    
    protected Percept() {
       // Exists only to defeat instantiation.
    }

    /**
     * Initializes singleton percept object.
     * 
     * @param loadTime timeout for initializing and setting up the bot on turn 0
     * @param turnTime timeout for a single game turn, starting with turn 1
     * @param rows game map height
     * @param cols game map width
     * @param turns maximum number of turns the game will be played
     * @param viewRadius2 squared view radius of each ant
     * @param attackRadius2 squared attack radius of each ant
     * @param spawnRadius2 squared spawn radius of each ant
     */
    public void initialize(int loadTime, int turnTime, int rows, int cols, int turns, int viewRadius2,
            int attackRadius2, int spawnRadius2) {
        this.loadTime = loadTime;
        this.turnTime = turnTime;
        this.rows = rows;
        this.cols = cols;
        this.turns = turns;
        this.viewRadius2 = viewRadius2;
        this.attackRadius2 = attackRadius2;
        this.spawnRadius2 = spawnRadius2;
        map = new Ilk[rows][cols];
        for (Ilk[] row : map) {
            Arrays.fill(row, Ilk.LAND);
        }
        killCount = new int[16];
        
//        for (int r = 0; r < rows; r++) {
//            for (int c = 0; c < cols; c++) {
//                    Tile tile = new Tile(r,c);
//                    FactRepository.getInstance().addInstance(new UnexploredFact(tile));
//            }
//        }
        
    }
    
    public static Percept getInstance() {
        if(instance == null) {
            instance = new Percept();
        }
        return instance;
    }
    /**
     * Returns timeout for initializing and setting up the bot on turn 0.
     * 
     * @return timeout for initializing and setting up the bot on turn 0
     */
    public int getLoadTime() {
        return loadTime;
    }
    
    /**
     * Returns timeout for a single game turn, starting with turn 1.
     * 
     * @return timeout for a single game turn, starting with turn 1
     */
    public int getTurnTime() {
        return turnTime;
    }
    
    /**
     * Returns game map height.
     * 
     * @return game map height
     */
    public int getRows() {
        return rows;
    }
    
    /**
     * Returns game map width.
     * 
     * @return game map width
     */
    public int getCols() {
        return cols;
    }
    
    /**
     * Returns maximum number of turns the game will be played.
     * 
     * @return maximum number of turns the game will be played
     */
    public int getTurns() {
        return turns;
    }
    
    /**
     * Returns squared view radius of each ant.
     * 
     * @return squared view radius of each ant
     */
    public int getViewRadius2() {
        return viewRadius2;
    }
    
    /**
     * Returns squared attack radius of each ant.
     * 
     * @return squared attack radius of each ant
     */
    public int getAttackRadius2() {
        return attackRadius2;
    }
    
    /**
     * Returns squared spawn radius of each ant.
     * 
     * @return squared spawn radius of each ant
     */
    public int getSpawnRadius2() {
        return spawnRadius2;
    }
    
    /**
     * Sets turn start time.
     * 
     * @param turnStartTime turn start time
     */
    public void setTurnStartTime(long turnStartTime) {
        this.turnStartTime = turnStartTime;
    }
    
    /**
     * Returns how much time the bot has still has to take its turn before timing out.
     * 
     * @return how much time the bot has still has to take its turn before timing out
     */
    public int getTimeRemaining() {
        return turnTime - (int)(System.currentTimeMillis() - turnStartTime);
    }
    
    /**
     * Returns ilk at the specified location.
     * 
     * @param tile location on the game map
     * 
     * @return ilk at the <cod>tile</code>
     */
    public Ilk getIlk(Tile tile) {
        return map[tile.getRow()][tile.getCol()];
    }
    
    /**
     * Sets ilk at the specified location.
     * 
     * @param tile location on the game map
     * @param ilk ilk to be set at <code>tile</code>
     */
    public void setIlk(Tile tile, Ilk ilk) {
        map[tile.getRow()][tile.getCol()] = ilk;
    }
    
    /**
     * Returns ilk at the location in the specified direction from the specified location.
     * 
     * @param tile location on the game map
     * @param direction direction to look up
     * 
     * @return ilk at the location in <code>direction</code> from <cod>tile</code>
     */
    public Ilk getIlk(Tile tile, Aim direction) {
        Tile newTile = getTile(tile, direction);
        return map[newTile.getRow()][newTile.getCol()];
    }
    
    /**
     * Returns location in the specified direction from the specified location.
     * 
     * @param tile location on the game map
     * @param direction direction to look up
     * 
     * @return location in <code>direction</code> from <cod>tile</code>
     */
    public Tile getTile(Tile tile, Aim direction) {
        int row = (tile.getRow() + direction.getRowDelta()) % rows;
        if (row < 0) {
            row += rows;
        }
        int col = (tile.getCol() + direction.getColDelta()) % cols;
        if (col < 0) {
            col += cols;
        }
        return new Tile(row, col);
    }
    
    public Aim getNeighborAim(Tile t1, Tile t2) throws InvalidException {
        List<Aim> directions = new ArrayList<Aim>();
        directions.add(Aim.NORTH);
        directions.add(Aim.SOUTH);
        directions.add(Aim.EAST);
        directions.add(Aim.WEST);
        
        for (Aim aim : directions) {
            if (getTile(t1,aim).equals(t2)) return aim;
        }
        
        throw new InvalidException("Neighbors aren't adjacent");
    }
    
    public List<Tile> getNeighbors(Tile tile) {
        List<Tile> results = new ArrayList<Tile> ();
        List<Aim> directions = new ArrayList<Aim>();
        directions.add(Aim.NORTH);
        directions.add(Aim.SOUTH);
        directions.add(Aim.EAST);
        directions.add(Aim.WEST);
        
        for (Aim aim : directions) {
            Tile targetTile = getTile(tile,aim);
            Ilk ilk = getIlk(targetTile);
            
            if (ilk.isPassable() && !Percept.getInstance().getMyHills().contains(targetTile)) results.add(getTile(tile,aim));
        }
        
        return results;
    }
    
    /**
     * Returns a set containing all unexplored tiles.
     * 
     * @return a set containing all unexplored tiles.
     */
    public Set<Fact> getUnexplored() {
        return FactRepository.getInstance().getInstances(UnexploredFact.class);
    }
    
    /**
     * Returns a set containing all my ants locations.
     * 
     * @return a set containing all my ants locations
     */
    public Set<Fact> getMyAnts() {
        return FactRepository.getInstance().getInstances(FriendlyAntFact.class);
    }
    
    /**
     * Returns a set containing all enemy ants locations.
     * 
     * @return a set containing all enemy ants locations
     */
    public Set<Fact> getEnemyAnts() {
        return FactRepository.getInstance().getInstances(EnemyAntFact.class);
    }
    
    /**
     * Returns a set containing all my hills locations.
     * 
     * @return a set containing all my hills locations
     */
    public Set<Fact> getMyHills() {
        return FactRepository.getInstance().getInstances(FriendlyHillFact.class);
    }
    
    /**
     * Returns a set containing all enemy hills locations.
     * 
     * @return a set containing all enemy hills locations
     */
    public Set<Fact> getEnemyHills() {
        return FactRepository.getInstance().getInstances(EnemyHillFact.class);
    }
    
    /**
     * Returns a set containing all food locations.
     * 
     * @return a set containing all food locations
     */
    public Set<Fact> getFoodTiles() {
        return FactRepository.getInstance().getInstances(FoodFact.class);
    }
    
    
    /**
     * Calculates distance between two locations on the game map.
     * 
     * @param t1 one location on the game map
     * @param t2 another location on the game map
     * 
     * @return distance between <code>t1</code> and <code>t2</code>
     */
    public int getDistance2(Tile t1, Tile t2) {
        int rowDelta = Math.abs(t1.getRow() - t2.getRow());
        int colDelta = Math.abs(t1.getCol() - t2.getCol());
        rowDelta = Math.min(rowDelta, rows - rowDelta);
        colDelta = Math.min(colDelta, cols - colDelta);
        return rowDelta*rowDelta + colDelta*colDelta;
    }
    
    /**
     * Returns one or two orthogonal directions from one location to the another.
     * 
     * @param t1 one location on the game map
     * @param t2 another location on the game map
     * 
     * @return orthogonal directions from <code>t1</code> to <code>t2</code>
     */
    @SuppressWarnings("unchecked")
    public List<Aim> getDirections(Tile t1, Tile t2) throws InvalidException {
        
        List<Aim> aims = new ArrayList<Aim>();
        
        AStarSearch search = new AStarSearch();
        List<Tile> tiles = search.findPath(t1, t2);
//        System.err.println("Path contains step count of "+tiles.size());

        // no path found
        if (tiles == null) throw new InvalidException("No path found");
        
        Tile last = t1;
        for (Tile tile : tiles) {
            aims.add(getNeighborAim(last,tile));
//            System.err.println("Adding aim: "+getNeighborAim(last,tile)+" to get to "+tile);
            last = tile;
        }
        
        return aims;
    }
    
    /**
     * Clears game state information about my ants locations.
     */
    public void clearMyAnts() {
        for (Fact myAnt : FactRepository.getInstance().getInstances(FriendlyAntFact.class)) {
            map[myAnt.getLocation().getRow()][myAnt.getLocation().getCol()] = Ilk.LAND;
        }
        FactRepository.getInstance().removeAllInstances(FriendlyAntFact.class);
    }
    
    /**
     * Clears game state information about enemy ants locations.
     */
    public void clearEnemyAnts() {
        for (Fact enemyAnt : FactRepository.getInstance().getInstances(EnemyAntFact.class)) {
            map[enemyAnt.getLocation().getRow()][enemyAnt.getLocation().getCol()] = Ilk.LAND;
        }
        FactRepository.getInstance().removeAllInstances(EnemyAntFact.class);
    }
    
    /**
     * Clears game state information about my hills locations.
     */
    public void clearMyHills() {
        FactRepository.getInstance().removeAllInstances(FriendlyHillFact.class);
    }
    
    /**
     * Clears game state information about enemy hills locations.
     */
    public void clearEnemyHills() {
        FactRepository.getInstance().removeAllInstances(EnemyHillFact.class);
    }
    
    /**
     * Updates game state information about new ants and food locations.
     * 
     * @param ilk ilk to be updated
     * @param tile location on the game map to be updated
     */
    public void update(Ilk ilk, Tile tile, int owner) {
        map[tile.getRow()][tile.getCol()] = ilk;
        switch (ilk) {
            case FOOD:
                FactRepository.getInstance().addInstance(new FoodFact(tile));
            break;
            case MY_ANT:
                FactRepository.getInstance().addInstance(new FriendlyAntFact(tile));
                FactRepository.getInstance().addInstance(new IntendedLocationFact(tile));
            break;
            case ENEMY_ANT:
                FactRepository.getInstance().addInstance(new EnemyAntFact(tile,owner));
                break;
            case DEAD:
                killCount[owner]++;
            break;
        }
    }
    
    public Set<Fact> getNewlyExploredTiles(Tile tile) {
        Set<Fact> explored = new HashSet<Fact> ();
        
        for (Fact unexp : FactRepository.getInstance().getInstances(UnexploredFact.class)) {
            int dc = unexp.getLocation().getCol() - tile.getCol();
            int dr = unexp.getLocation().getRow() - tile.getRow();
            
            if ( (dc*dc+dr*dr) < viewRadius2) explored.add(unexp);
        }
        
        return explored;
    }
    
    /**
     * Updates game state information about hills locations.
     *
     * @param owner owner of hill
     * @param tile location on the game map to be updated
     */
    public void updateHills(int owner, Tile tile) {
        if (owner > 0) {
            FactRepository.getInstance().addInstance(new EnemyHillFact(tile));
        } else {
            FactRepository.getInstance().addInstance(new FriendlyHillFact(tile));
        }
    }

    String getKillString() {
        String kills="";
        
        for (int i=0; i<16; i++) {
            if (killCount[i] > 0) kills += " "+i+": "+killCount[i];
        }
        
        return kills;
    }
    
}
